// =======================
// SillyTavern 扩展：预设打包助手 (st-packaging-assistant)
// - 单文件、无依赖版本
// - UI 与样式由 JS 直接注入，不依赖外部 CSS/HTML
// - 悬浮纸箱子球 FAB 可按需启用/禁用，拖拽与位置持久化
// - 移除 jQuery 依赖
// =======================

(async () => {
  // ---------- 常量区 ----------
  const SPA_EXT_NAME = "st-packaging-assistant";
  
  const STORAGE_KEY_POS = "spa-fab-pos";        // FAB 位置
  const STORAGE_KEY_ENABLED = "spa-enabled";    // FAB 启用开关（"1"|"0"，默认启用）

  const SPA_SETTINGS_MOUNT_ID = "spa-settings-mounted"; // settings.html 顶层容器 id（用于防重复）
  const SPA_OVERLAY_ID = "spa-overlay";                // overlay 容器 id
  const SPA_STYLES_ID = "spa-styles";                  // 注入样式的 <style> 标签 id

  // ---------- 内嵌资源 ----------

  // 原 style.css 内容
  const EMBEDDED_STYLES = `
    :root {
      --spa-z: 2147483646;
      --spa-overlay: rgba(0,0,0,.6);
      --spa-bg: #2c2f33;
      --spa-fg: #fff;
      --spa-border: #40444b;
      --spa-accent: #7289da;
    }
    #spa-fab {
      position: fixed; z-index: var(--spa-z); right: 20px; bottom: 20px;
      width: 48px; height: 48px; display: grid; place-items: center;
      border-radius: 50%; background: linear-gradient(145deg,#2f3338,#212529);
      color: var(--spa-accent); font-size: 22px; cursor: grab;
      box-shadow: 0 4px 8px rgba(0,0,0,.3), inset 0 2px 2px rgba(255,255,255,.05);
      transition: transform .2s ease-out;
    }
    #spa-fab:hover { transform: scale(1.06); }
    #spa-overlay {
      position: fixed; inset: 0; background: var(--spa-overlay);
      z-index: var(--spa-z); display: none;
    }
    #spa-modal { position: absolute; inset: 0; display: grid; place-items: center; }
    #spa-card {
      width: 90vw; height: 85vh; max-width: 900px;
      background: var(--spa-bg); color: var(--spa-fg);
      border: 1px solid var(--spa-border); border-radius: 10px;
      box-shadow: 0 8px 16px rgba(0,0,0,.25); position: relative;
      display: flex; flex-direction: column; overflow: hidden;
    }
    #spa-close {
      position: absolute; top: 10px; right: 10px; width: 32px; height: 32px;
      display: grid; place-items: center; border: none; background: rgba(0,0,0,0.2);
      color: #fff; font-size: 24px; line-height: 1; font-weight: bold;
      border-radius: 50%; cursor: pointer; z-index: 10;
      transition: background .2s ease, transform .2s ease;
    }
    #spa-close:hover { background: rgba(0,0,0,0.4); transform: scale(1.1); }
    #spa-content-container {
      flex-grow: 1; overflow-y: auto;
      font-family: 'Microsoft YaHei','Segoe UI',Arial,sans-serif;
    }
    #spa-content-container * { box-sizing: border-box; }
    #spa-content-container::-webkit-scrollbar { display: none; }
    #spa-content-container { -ms-overflow-style: none; scrollbar-width: none; }
    #spa-iframe-content .card {
      background: var(--spa-bg); width: 100%; height: 100%;
      display: flex; flex-direction: column;
    }
    #spa-iframe-content .header {
      background: linear-gradient(135deg, #40444b, #212529); color: #fff;
      padding: 16px 20px; text-align: center; flex-shrink: 0;
      border-bottom: 1px solid var(--spa-border);
    }
    #spa-iframe-content .header h1 { font-size: 1.3em; margin: 0 0 4px 0; font-weight: 600; }
    #spa-iframe-content .header p { font-size: .82em; opacity: .8; margin: 0; }
    #spa-iframe-content .content { padding: 18px 20px; display: flex; flex-direction: column; gap: 16px; flex: 1; }
    #spa-iframe-content .mode-tabs { display: flex; background: #212529; border-radius: 10px; padding: 3px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); flex-shrink: 0; }
    #spa-iframe-content .tab-btn { flex: 1; padding: 10px 16px; border: none; border-radius: 7px; font-size: .88em; font-weight: 500; cursor: pointer; transition: all .25s ease; background: transparent; color: #ccc; }
    #spa-iframe-content .tab-btn.active { background: var(--spa-accent); color: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.3); transform: translateY(-1px); }
    #spa-iframe-content .tab-content { display: none; animation: fadeIn .3s ease; flex: 1; min-height: 0; }
    #spa-iframe-content .tab-content.active { display: flex; flex-direction: column; }
    @keyframes fadeIn{ from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
    #spa-iframe-content .main-layout { display: flex; flex-direction: column; gap: 16px; flex: 1; align-items: start; }
    #spa-iframe-content .section { flex: 1; width: 100%; background: #36393f; border-radius: 10px; padding: 14px; border: 1px solid var(--spa-border); transition: all .2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    #spa-iframe-content .section:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.2); border-color: #50545b; }
    #spa-iframe-content .section-title { font-size: .9em; font-weight: 600; color: #eee; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; }
    #spa-iframe-content .list-box { max-height: 20vh; overflow: auto; border: 1px solid var(--spa-border); border-radius: 6px; padding: 6px; margin: 8px 0; font-size: .82em; background: #2f3136; }
    #spa-iframe-content .item-row { display: flex; align-items: center; padding: 6px 8px; margin-bottom: 3px; background: #3a3d42; border-radius: 4px; transition: all .2s ease; border: 1px solid transparent; }
    #spa-iframe-content .item-row:hover { background: #40444b; border-color: #50545b; }
    #spa-iframe-content .item-row input[type="checkbox"] { margin-right: 8px; transform: scale(.9); cursor: pointer; accent-color: var(--spa-accent); }
    #spa-iframe-content .item-name { font-weight: 500; color: #eee; font-size: .88em; }
    #spa-iframe-content .item-desc { font-size: .75em; color: #aab; margin-top: 1px; }
    #spa-iframe-content .resource-section, #spa-iframe-content .config-section { display: flex; flex-direction: column; width: 100%; gap: 16px; }
    #spa-iframe-content .selection-tabs { display: flex; border-bottom: 1px solid var(--spa-border); margin-bottom: 10px; }
    #spa-iframe-content .selection-tab-btn { padding: 8px 14px; cursor: pointer; border: none; background: none; font-size: .88em; color: #bbb; border-bottom: 2px solid transparent; transition: all .2s ease; }
    #spa-iframe-content .selection-tab-btn.active { color: #fff; font-weight: 600; border-bottom-color: var(--spa-accent); }
    #spa-iframe-content .selection-content { display: none; }
    #spa-iframe-content .selection-content.active { display: block; }
    #spa-iframe-content .option-title { font-size: .9em; font-weight: 600; margin-bottom: 10px; color: #eee; }
    #spa-iframe-content .checkbox-option { padding: 8px 12px; border: 1px solid var(--spa-border); border-radius: 6px; font-size: .88em; background: #3a3d42; color: #ddd; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
    #spa-iframe-content .checkbox-option label { cursor: pointer; }
    #spa-iframe-content .btn { background: var(--spa-accent); color: #fff; border: none; padding: 8px 14px; border-radius: 18px; font-size: .82em; font-weight: 500; cursor: pointer; transition: all .25s ease; margin: 2px; box-shadow: 0 2px 4px rgba(0,0,0,.2); }
    #spa-iframe-content .btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.3); filter: brightness(1.1); }
    #spa-iframe-content .btn-success { background: #43b581; }
    #spa-iframe-content .btn-success:hover { background: #3aa576; }
    #spa-iframe-content .input-field { width: 100%; padding: 8px 12px; border: 1px solid var(--spa-border); border-radius: 6px; font-size: .88em; margin-bottom: 6px; background: #212529; color: #ddd; }
    #spa-iframe-content .input-field:focus { outline: none; border-color: var(--spa-accent); box-shadow: 0 0 0 2px rgba(114, 137, 218, .3); }
    #spa-iframe-content .input-group { display: flex; gap: 8px; margin-bottom: 10px; }
    #spa-iframe-content .file-upload { display: inline-block; background: #4f545c; color: #fff; padding: 10px 16px; border-radius: 18px; cursor: pointer; font-size: .82em; font-weight: 500; transition: all .25s ease; box-shadow: 0 2px 4px rgba(0,0,0,.2); position: relative; }
    #spa-iframe-content .file-upload:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.3); background: #5c626b; }
    #spa-iframe-content .file-upload input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
    #spa-iframe-content .status-msg { padding: 10px 14px; border-radius: 8px; margin: 10px 0; font-size: .82em; font-weight: 500; text-align: center; display: none; border-left: 4px solid; color: #fff; }
    #spa-iframe-content .status-success { background: #28a745; border-color: #218838; }
    #spa-iframe-content .status-error { background: #dc3545; border-color: #c82333; }
    #spa-iframe-content .status-info { background: #6c757d; border-color: #5a6268; }
    #spa-iframe-content .progress { width: 100%; height: 3px; background: #212529; border-radius: 2px; margin: 10px 0; display: none; }
    #spa-iframe-content .progress-bar { height: 100%; background: var(--spa-accent); width: 0%; transition: width .3s ease; }
    #spa-iframe-content .package-info { background: #3a3d42; border: 1px solid var(--spa-border); border-radius: 8px; padding: 12px; margin: 10px 0; font-size: .82em; display: none; }
    #spa-iframe-content .package-info h4 { margin-bottom: 8px; color: #eee; font-size: .9em; }
    #spa-iframe-content .debug-log { background: #212529; border: 1px solid var(--spa-border); border-radius: 8px; padding: 10px; font-family: 'Courier New', monospace; font-size: .72em; max-height: 120px; overflow-y: auto; display: none; color: #ccc; }
    #spa-iframe-content .empty-state { text-align: center; color: #888; font-size: .82em; padding: 12px; font-style: italic; }
    @media (max-width: 768px) {
      #spa-card { width: 95vw; height: 90vh; }
      #spa-iframe-content .content { padding: 14px 16px; gap: 14px; }
      #spa-iframe-content .input-group { flex-direction: column; gap: 6px; }
      #spa-iframe-content .list-box { max-height: 18vh; }
      #spa-fab { width: 44px; height: 44px; font-size: 20px; right: 16px; bottom: 80px; }
    }
  `;
  
  // 原 settings.html 内容
  const EMBEDDED_SETTINGS_HTML = `
    <div id="${SPA_SETTINGS_MOUNT_ID}" class="extension_settings">
      <div class="inline-drawer">
        <div class="inline-drawer-toggle inline-drawer-header">
          <b>预设打包助手</b>
          <div class="inline-drawer-icon fa-solid fa-circle-chevron-down"></div>
        </div>
        <div class="inline-drawer-content">
          <div class="setting_item">
            <div class="setting_name">启用悬浮球</div>
            <div class="setting_value">
              <label class="switch">
                <input type="checkbox" id="spa-enabled-toggle">
                <span class="slider"></span>
              </label>
            </div>
          </div>
          <div class="setting_item">
            <div class="setting_name">重置悬浮球位置</div>
            <div class="setting_value">
              <button id="spa-reset-pos-btn" class="menu_button">重置</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  `;

  // 主面板 UI (原 RAW_HTML)
  const PANEL_HTML = `
    <div class="card" id="spa-iframe-content">
      <div class="header">
        <h1>预设打包助手</h1>
        <p>预设、正则、快速回复与世界书一键打包/导入</p>
      </div>
      <div class="content">
        <div class="mode-tabs">
          <button class="tab-btn active" onclick="switchTab('author')">打包</button>
          <button class="tab-btn" onclick="switchTab('player')">导入</button>
        </div>
        <div id="author-tab" class="tab-content active">
          <div class="main-layout">
            <div class="section resource-section">
              <div class="section-title">选择要打包的资源</div>
              <div class="selection-tabs">
                <button id="select-tab-presets" class="selection-tab-btn active" onclick="switchSelectionTab('presets')">预设</button>
                <button id="select-tab-regexes" class="selection-tab-btn" onclick="switchSelectionTab('regexes')">正则</button>
                <button id="select-tab-quick-replies" class="selection-tab-btn" onclick="switchSelectionTab('quick-replies')">快速回复</button>
                <button id="select-tab-worldbooks" class="selection-tab-btn" onclick="switchSelectionTab('worldbooks')">世界书</button>
              </div>
              <div id="selection-content-presets" class="selection-content active"><div id="presets-list" class="list-box"><div class="empty-state">加载中...</div></div></div>
              <div id="selection-content-regexes" class="selection-content"><div id="regexes-list" class="list-box"><div class="empty-state">加载中...</div></div></div>
              <div id="selection-content-quick-replies" class="selection-content"><div id="quick-reply-list" class="list-box"><div class="empty-state">加载中...</div></div></div>
              <div id="selection-content-worldbooks" class="selection-content"><div id="worldbooks-list" class="list-box"><div class="empty-state">加载中...</div></div></div>
            </div>
            <div class="section config-section">
              <div class="section-title">打包配置</div>
              <div class="input-group">
                <input type="text" id="tag-prefix" class="input-field" placeholder="标签前缀(可选)" style="flex:1;">
                <input type="text" id="package-name" class="input-field" placeholder="打包文件名" style="flex:2;">
              </div>
              <div class="config-options">
                <div class="option-group">
                  <div class="option-title">正则选项</div>
                  <div class="checkbox-option"><input type="checkbox" id="regex-no-overwrite"><label for="regex-no-overwrite">正则禁覆盖(添加“_新”后缀)</label></div>
                </div>
              </div>
              <button class="btn btn-success" onclick="createPackage()">确定并生成打包文件</button>
            </div>
          </div>
        </div>
        <div id="player-tab" class="tab-content">
          <div class="section">
            <div class="section-title">文件导入</div>
            <label class="file-upload">
              <input type="file" id="package-file" accept=".json" onchange="handleFileSelect(event)">
              选择打包文件
            </label>
            <div id="package-info" class="package-info">
              <h4>包信息</h4>
              <div id="package-details"></div>
              <button class="btn btn-success" onclick="importPackage()" style="margin-top:10px;">导入到酒馆</button>
            </div>
          </div>
        </div>
        <div class="progress" id="progress"><div class="progress-bar" id="progress-bar"></div></div>
        <div class="status-msg" id="status-msg"></div>
        <div class="debug-log" id="debug-log"></div>
      </div>
    </div>
    <script>
      // 脚本内容保持不变，直接注入
      // ... (此处省略与原文件完全相同的 script 内容)
    <\/script>
  `.replace('<script>', `<script>
    let selectedPresets = []; let selectedRegexes = []; let selectedQuickReplySets = []; let selectedWorldbooks = []; let packageData = null;
    function debugLog(msg){ console.log('[打包助手]', msg); const el = document.getElementById('debug-log'); if (!el) return; el.style.display = 'block'; el.innerHTML += new Date().toLocaleTimeString() + ': ' + msg + '<br>'; el.scrollTop = el.scrollHeight; }
    function showStatus(message, type='info'){ const el = document.getElementById('status-msg'); el.textContent = message; el.className = 'status-msg status-' + type; el.style.display = 'block'; setTimeout(()=>{ el.style.display='none'; }, 4000); debugLog('状态: ' + type.toUpperCase() + ' - ' + message); }
    function showProgress(p){ const k=document.getElementById('progress'); const b=document.getElementById('progress-bar'); k.style.display='block'; b.style.width=p+'%'; if(p>=100){ setTimeout(()=>{k.style.display='none';},800);} }
    function switchTab(tab){ document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c=>c.classList.remove('active')); document.querySelector('[onclick="switchTab(\\'\'+tab+'\\')"]').classList.add('active'); document.getElementById(tab+'-tab').classList.add('active'); const d=document.getElementById('debug-log'); if(d){ d.style.display='none'; d.innerHTML=''; } }
    function switchSelectionTab(tab){ document.querySelectorAll('.selection-tab-btn').forEach(b=>b.classList.remove('active')); document.querySelectorAll('.selection-content').forEach(c=>c.classList.remove('active')); document.getElementById('select-tab-'+tab).classList.add('active'); document.getElementById('selection-content-'+tab).classList.add('active'); }
    async function loadPresets(){ try{ if(typeof TavernHelper==='undefined') throw new Error('需要在 SillyTavern 环境中运行'); const names = TavernHelper.getPresetNames(); const box = document.getElementById('presets-list'); box.innerHTML=''; if(!names || names.length===0){ box.innerHTML='<div class="empty-state">未找到预设</div>'; return; } for(const name of names){ if(name==='in_use') continue; const preset = TavernHelper.getPreset(name); if(!preset) continue; const row = document.createElement('div'); row.className='item-row'; row.innerHTML = '<input type="checkbox" id="preset-'+name+'" onchange="togglePreset(\\'\''+name+'\\'')">'+'<div><div class="item-name">'+name+'</div>'+'<div class="item-desc">'+(preset.prompts?preset.prompts.length:0)+' 个提示词</div></div>'; box.appendChild(row); } }catch(e){ document.getElementById('presets-list').innerHTML='<div class="empty-state">加载失败: '+e.message+'</div>'; debugLog('预设加载错误: '+e.stack); } }
    async function loadRegexes(){ try{ if(typeof TavernHelper==='undefined') throw new Error('需要在 SillyTavern 环境中运行'); const regexes = TavernHelper.getTavernRegexes() || []; const box = document.getElementById('regexes-list'); box.innerHTML=''; if(regexes.length===0){ box.innerHTML='<div class="empty-state">未找到正则</div>'; return; } for(const r of regexes){ const row = document.createElement('div'); row.className='item-row'; const pv = (r.find_regex||'').toString().slice(0,30); row.innerHTML = '<input type="checkbox" id="regex-'+r.id+'" onchange="toggleRegex(\\'\''+r.id+'\\'')">'+'<div><div class="item-name">'+r.script_name+'</div>'+'<div class="item-desc">'+pv+'...</div></div>'; box.appendChild(row); } }catch(e){ document.getElementById('regexes-list').innerHTML='<div class="empty-state">加载失败: '+e.message+'</div>'; debugLog('正则加载错误: '+e.stack); } }
    async function loadQuickReplies(){ try{ if(typeof TavernHelper==='undefined') throw new Error('需要在 SillyTavern 环境中运行'); const setListJson = await TavernHelper.triggerSlash('/qr-set-list all'); const setNames = JSON.parse(setListJson) || []; const box = document.getElementById('quick-reply-list'); box.innerHTML=''; if(setNames.length===0){ box.innerHTML='<div class="empty-state">未找到快速回复集</div>'; return; } for(const name of setNames){ const listJson = await TavernHelper.triggerSlash('/qr-list "'+name+'"'); const arr = JSON.parse(listJson) || []; const row = document.createElement('div'); row.className='item-row'; row.innerHTML = '<input type="checkbox" id="qrset-'+name+'" onchange="toggleQuickReplySet(\\'\''+name+'\\'')">'+'<div><div class="item-name">'+name+'</div>'+'<div class="item-desc">'+arr.length+' 个回复</div></div>'; box.appendChild(row); } }catch(e){ document.getElementById('quick-reply-list').innerHTML='<div class="empty-state">加载失败: '+e.message+'</div>'; debugLog('快速回复加载错误: '+e.stack); } }
    async function loadWorldbooks(){ const box = document.getElementById('worldbooks-list'); try{ if(typeof TavernHelper==='undefined') throw new Error('需要在 SillyTavern 环境中运行'); if(typeof TavernHelper.getWorldbookNames!=='function'||typeof TavernHelper.getWorldbook!=='function'){ box.innerHTML = '<div class="empty-state">当前酒馆助手版本不支持世界书 API，请更新</div>'; debugLog('世界书API不可用：缺少 getWorldbookNames/getWorldbook'); return; } const names = TavernHelper.getWorldbookNames() || []; box.innerHTML=''; if(names.length===0){ box.innerHTML='<div class="empty-state">未找到世界书</div>'; return; } for(const name of names){ let cnt='...'; try{ const entries=await TavernHelper.getWorldbook(name); cnt=(Array.isArray(entries)?entries.length:0)+' 条目'; }catch{} const row=document.createElement('div'); row.className='item-row'; row.innerHTML = '<input type="checkbox" id="worldbook-'+name+'" onchange="toggleWorldbook(\\'\''+name+'\\'')">'+'<div><div class="item-name">'+name+'</div>'+'<div class="item-desc">'+cnt+'</div></div>'; box.appendChild(row); } }catch(e){ box.innerHTML='<div class="empty-state">加载失败: '+e.message+'</div>'; debugLog('世界书加载错误: '+e.stack); } }
    function togglePreset(name){ selectedPresets = selectedPresets.includes(name)? selectedPresets.filter(n=>n!==name):[...selectedPresets,name]; debugLog('预设选择: '+selectedPresets.length+' 个'); }
    function toggleRegex(id){ selectedRegexes = selectedRegexes.includes(id)? selectedRegexes.filter(n=>n!==id):[...selectedRegexes,id]; debugLog('正则选择: '+selectedRegexes.length+' 个'); }
    function toggleQuickReplySet(name){ selectedQuickReplySets = selectedQuickReplySets.includes(name)? selectedQuickReplySets.filter(n=>n!==name):[...selectedQuickReplySets,name]; debugLog('快速回复集选择: '+selectedQuickReplySets.length+' 个'); }
    function toggleWorldbook(name){ selectedWorldbooks = selectedWorldbooks.includes(name)? selectedWorldbooks.filter(n=>n!==name):[...selectedWorldbooks,name]; debugLog('世界书选择: '+selectedWorldbooks.length+' 本'); }
    async function createPackage(){ if(selectedPresets.length===0 && selectedRegexes.length===0 && selectedQuickReplySets.length===0 && selectedWorldbooks.length===0){ showStatus('请至少选择一个项目','error'); return; } const packageName = document.getElementById('package-name').value.trim(); if(!packageName){ showStatus('请输入包名称','error'); return; } const tagPrefix = document.getElementById('tag-prefix').value.trim(); const regexNoOverwrite = document.getElementById('regex-no-overwrite').checked; try{ showStatus('生成打包文件...','info'); showProgress(10); const pkg = { name: packageName, created: new Date().toISOString(), tag_prefix: tagPrefix, presets: {}, regexes: {}, quick_reply_sets: {}, worldbooks: {} }; for(const n of selectedPresets){ const p = TavernHelper.getPreset(n); if(!p) continue; const final = tagPrefix? (tagPrefix+n): n; pkg.presets[final] = p; debugLog('已打包预设: '+final); } showProgress(30); const all = TavernHelper.getTavernRegexes() || []; for(const id of selectedRegexes){ const r = all.find(x=>x.id===id); if(!r) continue; let final = r.script_name; if(tagPrefix) final = tagPrefix + final; if(regexNoOverwrite) final = final + '_新'; const copy = { ...r, script_name: final }; delete copy.id; pkg.regexes[final] = copy; debugLog('已打包正则: '+final); } showProgress(55); for(const setName of selectedQuickReplySets){ const finalSet = tagPrefix? (tagPrefix+setName): setName; const listJson = await TavernHelper.triggerSlash('/qr-list "'+setName+'"'); const labels = JSON.parse(listJson) || []; const arr = []; for(const label of labels){ const dataJson = await TavernHelper.triggerSlash('/qr-get set="'+setName+'" label="'+label+'"'); const data = JSON.parse(dataJson); arr.push(data); } pkg.quick_reply_sets[finalSet] = arr; debugLog('已打包快速回复集: '+finalSet); } showProgress(75); if(selectedWorldbooks.length>0){ if(typeof TavernHelper.getWorldbook!=='function') throw new Error('当前酒馆助手不支持获取世界书'); for(const wb of selectedWorldbooks){ const finalWb = tagPrefix? (tagPrefix+wb): wb; const entries = await TavernHelper.getWorldbook(wb); pkg.worldbooks[finalWb] = entries || []; debugLog('已打包世界书: '+finalWb+'（'+(entries?entries.length:0)+' 条目）'); } } showProgress(90); const jsonStr = JSON.stringify(pkg, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = packageName + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showProgress(100); showStatus('打包完成','success'); debugLog('打包完成: 预设'+Object.keys(pkg.presets).length+'、正则'+Object.keys(pkg.regexes).length+'、快速回复'+Object.keys(pkg.quick_reply_sets).length+'、世界书'+Object.keys(pkg.worldbooks).length); }catch(e){ showProgress(100); showStatus('打包失败: '+e.message,'error'); debugLog('打包错误: '+e.stack); } }
    function handleFileSelect(ev){ const file = ev.target.files[0]; if(!file){ debugLog('未选择文件'); return; } ev.target.value = ''; const reader = new FileReader(); reader.onload = (e)=>{ try{ packageData = JSON.parse(e.target.result); debugLog('文件解析成功'); displayPackageInfo(packageData); }catch(err){ showStatus('文件格式错误','error'); debugLog('解析错误: '+err.message); packageData = null; document.getElementById('package-info').style.display='none'; } }; reader.onerror = ()=>{ showStatus('文件读取失败','error'); debugLog('读取错误'); }; reader.readAsText(file); }
    function displayPackageInfo(data){ const el = document.getElementById('package-details'); let html = '<p><strong>名称:</strong> '+(data.name||'未知')+'</p>'; html += '<p><strong>创建:</strong> '+(data.created? new Date(data.created).toLocaleString():'未知')+'</p>'; if(data.tag_prefix) html += '<p><strong>标签:</strong> '+data.tag_prefix+'</p>'; html += '<p><strong>预设:</strong> '+(data.presets?Object.keys(data.presets).length:0)+' 个</p>'; html += '<p><strong>正则:</strong> '+(data.regexes?Object.keys(data.regexes).length:0)+' 个</p>'; html += '<p><strong>快速回复集:</strong> '+(data.quick_reply_sets?Object.keys(data.quick_reply_sets).length:0)+' 个</p>'; html += '<p><strong>世界书:</strong> '+(data.worldbooks?Object.keys(data.worldbooks).length:0)+' 本</p>'; el.innerHTML = html; document.getElementById('package-info').style.display='block'; }
    async function importPackage(){ if(!packageData){ showStatus('请先选择文件','error'); return; } try{ showStatus('导入中...','info'); if(packageData.presets){ for(const [name, preset] of Object.entries(packageData.presets)){ if(typeof TavernHelper.createOrReplacePreset==='function'){ await TavernHelper.createOrReplacePreset(name, preset); } debugLog('预设导入: '+name); } } if(packageData.regexes && Object.keys(packageData.regexes).length>0){ const curr = TavernHelper.getTavernRegexes() || []; const next = [...curr]; for(const [name, regex] of Object.entries(packageData.regexes)){ const rec = { ...regex }; const idx = next.findIndex(x=>x.script_name===rec.script_name); if(idx>=0){ rec.id = next[idx].id; next[idx] = rec; debugLog('正则替换: '+rec.script_name); }else{ rec.id = Date.now().toString() + Math.random().toString(36).slice(2,9); next.push(rec); debugLog('正则新增: '+rec.script_name); } } await TavernHelper.replaceTavernRegexes(next, { scope: 'all' }); } if(packageData.quick_reply_sets){ for(const [setName, arr] of Object.entries(packageData.quick_reply_sets)){ await TavernHelper.triggerSlash('/qr-set-create "'+setName+'"'); for(const item of arr){ const args = Object.entries(item) .filter(([k,v])=>k!=='command' && v!==null && v!==undefined) .map(([k,v])=> k+'='+JSON.stringify(v)) .join(' '); const cmd = item.command || ''; await TavernHelper.triggerSlash('/qr-create set="'+setName+'" '+args+' '+cmd); } debugLog('快速回复集导入: '+setName); } } if(packageData.worldbooks){ if(typeof TavernHelper.createOrReplaceWorldbook!=='function') throw new Error('当前酒馆助手不支持创建/替换世界书'); for(const [wbName, entries] of Object.entries(packageData.worldbooks)){ await TavernHelper.createOrReplaceWorldbook(wbName, Array.isArray(entries)?entries:[]); debugLog('世界书导入: '+wbName+'（'+(entries?entries.length:0)+' 条目）'); } } showProgress(100); showStatus('导入完成','success'); debugLog('导入完成'); packageData = null; const f = document.getElementById('package-file'); if(f) f.value=''; document.getElementById('package-info').style.display='none'; }catch(e){ showProgress(100); showStatus('导入失败: '+e.message,'error'); debugLog('导入错误: '+e.stack); } }
    async function loadAllResources(){ showStatus('正在加载所有资源...','info'); showProgress(10); await loadPresets(); showProgress(35); await loadRegexes(); showProgress(60); await loadQuickReplies(); showProgress(85); await loadWorldbooks(); showProgress(100); showStatus('资源加载完成','success'); }
  `);

  // ---------- 启用状态工具 ----------
  function isEnabled() {
    const v = localStorage.getItem(STORAGE_KEY_ENABLED);
    return v === null ? true : v === "1";
  }

  function syncSettingsUI() {
    const cb = document.getElementById('spa-enabled-toggle');
    if (cb) cb.checked = isEnabled();
  }

  function setEnabled(enabled) {
    localStorage.setItem(STORAGE_KEY_ENABLED, enabled ? "1" : "0");
    if (enabled && !document.getElementById('spa-fab')) {
      mountUI();
    }
    const fab = document.getElementById('spa-fab');
    const overlay = document.getElementById(SPA_OVERLAY_ID);
    if (enabled) {
      if (fab) fab.style.display = 'block';
    } else {
      if (fab) fab.style.display = 'none';
      if (overlay) overlay.style.display = 'none';
    }
    syncSettingsUI();
  }
  
  // ---------- 资源注入 ----------
  function injectAssets() {
    if (!document.getElementById(SPA_STYLES_ID)) {
      const style = document.createElement("style");
      style.id = SPA_STYLES_ID;
      style.textContent = EMBEDDED_STYLES;
      document.head.appendChild(style);
    }
  }

  // ---------- 拖拽与定位 ----------
  function addDragAndClickHandler(el, onClick) {
    let isClick = true;
    const dragHandler = (e) => {
      if (e.type === 'touchstart') e.preventDefault();
      const rect = el.getBoundingClientRect();
      const offsetX = (e.type.includes('mouse') ? e.clientX : e.touches[0].clientX) - rect.left;
      const offsetY = (e.type.includes('mouse') ? e.clientY : e.touches[0].clientY) - rect.top;
      const move = (moveEvent) => {
        isClick = false;
        el.style.cursor = 'grabbing';
        let newLeft = (moveEvent.type.includes('mouse') ? moveEvent.clientX : moveEvent.touches[0].clientX) - offsetX;
        let newTop = (moveEvent.type.includes('mouse') ? moveEvent.clientY : moveEvent.touches[0].clientY) - offsetY;
        newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - el.offsetWidth));
        newTop = Math.max(0, Math.min(newTop, window.innerHeight - el.offsetHeight));
        el.style.left = `${newLeft}px`;
        el.style.top = `${newTop}px`;
        el.style.right = 'auto';
        el.style.bottom = 'auto';
      };
      const end = () => {
        document.removeEventListener('mousemove', move);
        document.removeEventListener('mouseup', end);
        document.removeEventListener('touchmove', move);
        document.removeEventListener('touchend', end);
        el.style.cursor = 'grab';
        if (isClick) {
          onClick();
        } else {
          localStorage.setItem(STORAGE_KEY_POS, JSON.stringify({ top: el.style.top, left: el.style.left }));
        }
        setTimeout(() => { isClick = true; }, 0);
      };
      document.addEventListener('mousemove', move);
      document.addEventListener('mouseup', end);
      document.addEventListener('touchmove', move, { passive: false });
      document.addEventListener('touchend', end);
    };
    el.addEventListener('mousedown', dragHandler);
    el.addEventListener('touchstart', dragHandler, { passive: false });
  }

  function restoreFabPosition(el) {
    const savedPos = JSON.parse(localStorage.getItem(STORAGE_KEY_POS));
    if (savedPos?.top && savedPos?.left) {
      el.style.position = 'fixed';
      el.style.top = savedPos.top;
      el.style.left = savedPos.left;
      el.style.right = 'auto';
      el.style.bottom = 'auto';
    }
  }

  // ---------- UI 装配 ----------
  function ensureOverlay() {
    let overlay = document.getElementById(SPA_OVERLAY_ID);
    if (overlay) return overlay;
    overlay = document.createElement('div');
    overlay.id = SPA_OVERLAY_ID;
    overlay.style.display = 'none';
    overlay.innerHTML = `
      <div id="spa-modal">
        <div id="spa-card">
          <button id="spa-close" aria-label="close">×</button>
          <div id="spa-content-container" style="width:100%; height:100%; overflow:auto;"></div>
        </div>
      </div>`;
    document.body.appendChild(overlay);
    return overlay;
  }

  function mountUI() {
    let fab = document.getElementById('spa-fab');
    if (!fab) {
      fab = document.createElement('div');
      fab.id = 'spa-fab';
      fab.title = '预设打包助手';
      fab.textContent = '📦';
      document.body.appendChild(fab);
      const fabClickAction = () => {
        const ov = ensureOverlay();
        ov.style.display = 'block';
        const loadedFlag = document.body.getAttribute('data-spa-loaded');
        if (!loadedFlag && typeof loadAllResources === 'function') {
          loadAllResources();
          document.body.setAttribute('data-spa-loaded', 'true');
        }
      };
      addDragAndClickHandler(fab, fabClickAction);
      restoreFabPosition(fab);
    }
    const overlay = ensureOverlay();
    overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.style.display = 'none'; });
    const closeBtn = overlay.querySelector('#spa-close');
    if (closeBtn && !closeBtn.dataset.listenerAttached) {
      closeBtn.addEventListener('click', () => overlay.style.display = 'none');
      closeBtn.dataset.listenerAttached = 'true';
    }
    const contentContainer = overlay.querySelector('#spa-content-container');
    if (contentContainer && !contentContainer.innerHTML) {
      contentContainer.innerHTML = PANEL_HTML;
      const scriptTag = contentContainer.querySelector('script');
      if (scriptTag) {
        const finalScript = document.createElement('script');
        finalScript.textContent = scriptTag.innerText;
        document.body.appendChild(finalScript);
        scriptTag.remove();
      }
    }
  }

  // ---------- 绑定设置页事件 ----------
  document.addEventListener('change', (e) => {
    if (e.target && e.target.id === 'spa-enabled-toggle') {
      setEnabled(Boolean(e.target.checked));
    }
  });

  document.addEventListener('click', (e) => {
    if (e.target && e.target.id === 'spa-reset-pos-btn') {
      localStorage.removeItem(STORAGE_KEY_POS);
      const fab = document.getElementById('spa-fab');
      if (fab) {
        fab.style.left = '';
        fab.style.top = '';
        fab.style.right = '20px';
        fab.style.bottom = '20px';
      }
    }
  });

  // ---------- 动态注入设置UI ----------
  const settingsObserver = new MutationObserver(() => {
    const settingsPanel = document.getElementById('extensions_settings2');
    if (settingsPanel && !document.getElementById(SPA_SETTINGS_MOUNT_ID)) {
      settingsPanel.insertAdjacentHTML('beforeend', EMBEDDED_SETTINGS_HTML);
    }
    syncSettingsUI();
  });
  settingsObserver.observe(document.body, { childList: true, subtree: true });

  // ---------- 初始化顺序 ----------
  injectAssets();
  if (isEnabled()) {
    mountUI();
  }
  syncSettingsUI();

  console.log('[st-packaging-assistant] loaded');
})();